﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace System.Xml
{
    public class XmlParser
    {
        private List<string> CDataContent;

        /// <summary>
        /// Parse an Xml 1.0 string
        /// </summary>
        /// <param name="xml">xml string</param>
        /// <returns>Xmldocument</returns>
        public XmlDocument Parse(string xml)
        {
            XmlDocument xdoc = this.WriteXmlToDoucment(ParseXml(xml));
            return xdoc;
        }

        /// <summary>
        /// Parses the Xml string with regex into parts
        /// </summary>
        /// <param name="xmldata"></param>
        /// <returns></returns>
        private Match[] ParseXml(string xmldata)
        {
            xmldata = DefocusCData(xmldata);
            List<Match> matches = new List<Match>();

            //startpatt: <(?<ename>[\w]*)> | V2: <(?<etype>/*)(?<ename>[\w]*)>(?<innertext>[\w\s.\-:/]*)</
            //endpatt: >(?<innertext>[\w\s\-:/.]*)<
            //cdata: <!\[CDATA\[(?<cdata>.*)\]\]> | \w>(?<innertext>.*)<\w /.all
            string startpatterns = "<(?<etype>/*)(?<ename>[\\w]*)>";
            string innerpatterns = @">(?<innertext>[\w\s\-:/.]*)<";
            string cdatapatterns = @"<!\[CDATA\[(?<cdata>.*)\]\]>";

            //Add patterns to matchlist
            AddMatchCollectionToList(matches, Regex.Matches(xmldata, startpatterns, RegexOptions.Multiline));
            AddMatchCollectionToList(matches, Regex.Matches(xmldata, innerpatterns, RegexOptions.Multiline));
            AddMatchCollectionToList(matches, Regex.Matches(xmldata, cdatapatterns, RegexOptions.Multiline));

            //sort list
            matches.Sort(new MatchSorter());

            return matches.ToArray();
        }

        //Writes CData into an index
        private string DefocusCData(string xmldata)
        {
            int deletedlength = 0;
            string defXml = xmldata;
            string cdatapatterns = @"(?<=<!\[CDATA\[).*?(?=\]\])";
            CDataContent = new List<string>();

            MatchCollection cdataMatches = Regex.Matches(xmldata, cdatapatterns, RegexOptions.Singleline);

            foreach (Match match in cdataMatches)
            {
                CDataContent.Add(match.Value);
                defXml = defXml.Remove(match.Index - deletedlength, match.Value.Length);
                defXml = defXml.Insert(match.Index - deletedlength, (CDataContent.Count - 1).ToString());

                deletedlength += match.Value.Length - 1;
            }

            return defXml;
        }

        /// <summary>
        /// Fills a Matchcollection into a matchlist (Linq workaround)
        /// </summary>
        /// <param name="matchlist">List with matches</param>
        /// <param name="matchcol">Collection with matches</param>
        private void AddMatchCollectionToList(List<Match> matchlist, MatchCollection matchcol)
        {
            foreach (Match m in matchcol)
            {
                matchlist.Add(m);
            }
        }

        /// <summary>
        /// Writes Xml Data into XmlDocument
        /// </summary>
        /// <param name="matches">List of sorted matches</param>
        /// <returns>XmLDocument</returns>
        private XmlDocument WriteXmlToDoucment(Match[] matches)
        {
            XmlDocument xdoc = new XmlDocument();
            Stack<XmlNode> nodestack = new Stack<XmlNode>();

            nodestack.Push(xdoc);

            foreach (Match m in matches)
            {
                //Check Mtype
                if (m.Groups["etype"].Value == "/")
                {
                    //Element Close
                    nodestack.Pop();
                }
                else
                {
                    //Check for inner text
                    if (m.Groups["innertext"].Success)
                    {
                        nodestack.Peek().InnerText = m.Groups["innertext"].Value;
                    }
                    else if (m.Groups["cdata"].Success)
                    {
                        //check for cdata
                        XmlNode cdatandoe = new XmlNode();
                        cdatandoe.Value = CDataContent[Convert.ToInt32(m.Groups["cdata"].Value.Replace("]", ""))];
                        nodestack.Peek().ChildNodes.Add(cdatandoe);
                    }
                    else
                    {
                        //Element Open
                        XmlNode tempNode = new XmlNode();
                        tempNode.Name = m.Groups["ename"].Value;

                        nodestack.Peek().ChildNodes.Add(tempNode);
                        nodestack.Push(tempNode);
                    }
                }
            }

            return xdoc;
        }

        /// <summary>
        /// Sorts Match Data by it's index
        /// </summary>
        private class MatchSorter : IComparer<Match>
        {
            public int Compare(Match obj1, Match obj2)
            {
                return obj1.Index.CompareTo(obj2.Index);
            }
        }
    }
}
